In this project I made a game as solo developer with 2 game artists and made an amazing formula 1 replica game in 2 weeks. because I was a solo developer I had to do a lot of work to get the work done.



I first started creating the most important mechanic in the game: Movement. I brainstormed about what car mechanics I wanted to use and chose for a challenge with wheel colliders. Wheel colliders have lots of potential to good physics on a car movement mechanic, such as friction, suspension and stiffness.



I created a script for all 4 wheels assigning: TurnAngle, WheelCollider, Mesh and Powered(if the wheel uses acceleration or only rolls).

public class Wheel : MonoBehaviour
  {
      public bool powered = false;
      public float maxAngle = 90f;
      public float offset = 0f;
      public float rotationOffset = 90;
  
      private float turnAngle;
      private WheelCollider wcol;
      private Transform wmesh;
  
  
      private void Start()
      {
          wcol = GetComponentInChildren();
          wmesh = transform.Find("Wheel_Mesh");
      }
  
      private void Update()
      {
          Debug.Log(wcol.brakeTorque);
      }
  
      public void Steer(float steerInput)
      {
          turnAngle = steerInput * maxAngle + offset;
          wcol.steerAngle = turnAngle;
          wcol.motorTorque = 1000;
      }
  
      public void Accelerate(float powerInput)
      {
          if (powered)
              wcol.motorTorque = powerInput;
          else
              wcol.motorTorque = 0f;

      }
  
      public void UpdatePosition()
      {
          Vector3 pos = transform.position;
          Quaternion rot = transform.rotation;
  
          wcol.GetWorldPose(out pos, out rot);
          wmesh.transform.position = pos;
          wmesh.transform.rotation = rot *= Quaternion.Euler(0, rotationOffset, 0);
      }
  }
  
Random Image



Then I created a script for the main body of the car where input is added and also the wheels are attached to.

public class Formula : MonoBehaviour
  {
      [Header("Car Info")]
      public Transform gravityTarget;
      public Vector3 currentVelocity;
      public GameObject steeringWheel;
      public Transform speedOmeter;
      public GameObject throttle;
      public GameObject brakee;
      
      public float power = 15000f;
      public float torque = 500f;
      public float gravity = 9.81f;
      public float breakForce = 1f;
  
      [Header("UI")]
      public TextMeshProUGUI speedLabel;
      public float maxSpeed = 0.0f;
      private float speed = 0.0f;
  
      [Header("Car Cams")]
      public GameObject cameraOne;
      public GameObject cameraTwo;
      public GameObject cameraThree;
  
      private float fall = -0.2f;
  
      [Header("Car Orientation")]
      public bool autoOrient = false;
      public float autoOrientSpeed = 1f;
  
      private float horInput;
      private float verInput;
      private float brakeInput;
      private float steerAngle;
    
      [Header("Wheels")]
      public Wheel[] wheels;
  
      private Rigidbody rb;
  
      void Start()
      {
          throttle.SetActive(false);
          brakee.SetActive(false);
          rb = GetComponent<Rigidbody>();
      }
  
      void Update()
      {
          Cursor.visible = false;
  
          speed = rb.velocity.magnitude * 3.0f;
          speedLabel.text = ((int)speed + " km/h");
  
          ProcessInput();
          Vector3 diff = transform.position - gravityTarget.position;
          if (autoOrient) { AutoOrient(-diff); }
  
          speedOmeter.localEulerAngles = new Vector3(0, 0, Mathf.Lerp(180, -90, speed / 300));
        }
  
      void FixedUpdate()
      {
          rb.centerOfMass = new Vector3(0, fall, 0);
  
          ProcessForces();
          ProcessGravity();
      }
  
      void ProcessInput()
      {
          verInput = Input.GetAxis("Vertical");
          horInput = Input.GetAxis("Horizontal");
          brakeInput = Input.GetAxis("Jump");
      }
  
      void ProcessForces()
      {
          Vector3 force = new Vector3(0f, 0f, verInput * power);
          rb.AddRelativeForce(force);
    
          if (Input.GetKey(KeyCode.Space))
              rb.velocity = Vector3.Lerp(rb.velocity, Vector3.zero, Time.fixedDeltaTime * 1.0f);
  
          foreach (Wheel w in wheels)
          {
              w.Steer(horInput);
              w.Accelerate(verInput * power);
              w.UpdatePosition();
          }
      }
  
      void ProcessGravity()
      {
          Vector3 diff = transform.position - gravityTarget.position;
          rb.AddForce(-diff.normalized * gravity * (rb.mass));
      }
  
      void AutoOrient(Vector3 down)
      {
          Quaternion orientationDirection = Quaternion.FromToRotation(-transform.up, down) * transform.rotation;
          transform.rotation = Quaternion.Slerp(transform.rotation, orientationDirection, autoOrientSpeed * Time.deltaTime);
      }
  }
  
Random Image

It's a lot of public variables but that's mainly because debugging purpose and lack of time we had for the project and the to do list for me.



I also added the option to change the camera of the car to 3 position: third person view, car camera(slighlty above the player) and player point of view.

public class Formula : MonoBehaviour
  {
      [Header("Car Cams")]
      public GameObject cameraOne;
      public GameObject cameraTwo;
      public GameObject cameraThree;
  
      public bool camOne;
      public bool camTwo;
      public bool camThree;
  
      void Update()
      {
          CamChange();
      }
  
      public void CamChange()
      {
          if (Input.GetKeyUp(KeyCode.C))
          {
              if (camOne == true)
              {
                  cameraThree.SetActive(false);
                  cameraTwo.SetActive(false);
                  cameraOne.SetActive(true);
                  
                  camOne = false;
                  camTwo = true;
                  camThree = false;
              }
              else if (camTwo == true)
              {
                  cameraThree.SetActive(false);
                  cameraTwo.SetActive(true);
                  cameraOne.SetActive(false);
                  camOne = false;
                  camTwo = false;
                  camThree = true;
              }
              else if (camThree == true)
              {
                  cameraTwo.SetActive(false);
                  cameraThree.SetActive(true);
                  cameraOne.SetActive(false);
                  camOne = true;
                  camTwo = false;
                  camThree = false;
              }
              Debug.Log($" {camOne} {cameraTwo} {cameraThree}");
          }
  
          if (Input.GetKeyUp(KeyCode.Alpha1))
          {
              cameraThree.SetActive(false);
              cameraTwo.SetActive(false);
              cameraOne.SetActive(true);
          }
  
          if (Input.GetKeyUp(KeyCode.Alpha2))
          {
              cameraOne.SetActive(false);
              cameraThree.SetActive(false);
              cameraTwo.SetActive(true);
          }
  
          if (Input.GetKeyUp(KeyCode.Alpha3))
          {
              cameraTwo.SetActive(false);
              cameraThree.SetActive(true);
              cameraOne.SetActive(false);
          }
        }
  }
  

It's not the greatest system I created I know that but like I said because the lack of time we had I went for this system. if I would have changed it I would've changed it to an enum that has 3 state(3 cam positions) and have a function where the states are being switched by key input.
For example:

public enum CamState
  {
      camOne = 1
      camTwo,
      camThree
  }

  public class Formula : MonoBehaviour
  {
      [SerializeField] private CamState state;
      private int exampleNumber = 1;

      private void Update()
      {
          if (Input.GetKeyDown(KeyCode.C))
              ExampleFunction();
      }

      private void ExampleFunction ()
      {
        exampleNumber++;
        state = (CamState)exampleNumber;

        cameraOne.SetActive(false);
        cameraTwo.SetActive(false);
        cameraThree.SetActive(false);
        
          Switch(state)
          {
              case CamOne:
                cameraOne.SetActive(true);
              break:

              case CamTwo:
              cameraTwo.SetActive(true);
              break:

              case CamThree:
              cameraThree.SetActive(true);
              break:
          }
      }
  }

Much less space then before. But after the project we didn't continue developing it but if we did that was the first on my list.



After the car was working properly I went after the second most important thing in the game: Laptime. I created a script that doesn't only saves the laptimes but also converts them to minutes, seconds and milliseconds. As extra I created the function to add all times together into one final game time.

using UnityEngine.UI;
  using TMPro;
  
  public class GameUI : MonoBehaviour
  {
      public TextMeshProUGUI lapOne;
      public TextMeshProUGUI lapTwo;
      public TextMeshProUGUI lapThree;
      public TextMeshProUGUI totalTime;
  
      public float timerLapOne = 0;
      public float timerLapTwo = 0;
      public float timerLapThree = 0;
      public float totalTimer = 0;
  
      public bool timerOneActivate = false;
      public bool timerTwoActivate = false;
      public bool timerThreeActivate = false;
      public bool calculateTotaleTime = false;
  
      private BoxCollider boxCol;
      public GameObject finish;
      public GameObject speedoMeter;
      public GameObject speedOmeter;
  
      public Animator panel;
      public Animator laps;
  
      public GameObject finishCam;
  
      private float timerForScale = 0.5f;
      private bool activateIt = false;
  
      private void Start()
      {
          boxCol = GetComponent<BoxCollider>();
      }
  
      public void Update()
      {
          Debug.Log(timerForScale);
  
          if (activateIt)
          {
              timerForScale -= Time.deltaTime;
              if (timerForScale <= 0)
              {
                  Time.timeScale = 1;
                  Time.fixedDeltaTime = Time.timeScale * 1f;
              }
          }
  
  
          if (timerOneActivate)
          {
              lapOne.gameObject.SetActive(true);
  
              timerLapOne += Time.deltaTime;
  
              lapOne.text = string.Format("" + TimeString(timerLapOne, lapOne));
          }
          else if (timerTwoActivate)
          {
              lapTwo.gameObject.SetActive(true);
  
              timerLapTwo += Time.deltaTime;
              lapTwo.text = string.Format("" + TimeString(timerLapTwo, lapTwo));
          }
          else if (timerThreeActivate)
          {
              lapThree.gameObject.SetActive(true);
  
              timerLapThree += Time.deltaTime;
              lapThree.text = string.Format("" + TimeString(timerLapThree, lapThree));
          }
          else if (calculateTotaleTime)
          {
              totalTime.gameObject.SetActive(true);
  
              totalTimer = timerLapOne + timerLapTwo + timerLapThree;
              totalTime.text = string.Format("" + TimeString(totalTimer, totalTime));
  
              PlayerPrefs.SetFloat("RaceTime", totalTimer);
          }
      }
  
      public string TimeString(float lapTime, TextMeshProUGUI laptimer)
      {
          float mSec = (int)((lapTime - (int)lapTime) * 100);
          float sec = (int)(lapTime % 60);
          float min = (int)(lapTime / 60 % 60);
          return laptimer.text = string.Format("{0:00}:{1:00}:{2:00}", min, sec, mSec);
      }
  
      private void OnTriggerEnter(Collider other)
      {
          if (other.gameObject.CompareTag("FirstLap"))
          {
              if (timerOneActivate == false && timerTwoActivate == false && timerThreeActivate == false)
              {
                  Debug.Log("on");
                  timerOneActivate = true;
                  other.gameObject.SetActive(false);
              }
          }
          if (other.gameObject.CompareTag("NewLap"))
          {
              if (timerOneActivate)
              {
                  StartCoroutine(ColliderOn(other.gameObject));
                  timerOneActivate = false;
                  timerTwoActivate = true;
              }
              else if (timerTwoActivate)
              {
                  timerTwoActivate = false;
                  timerThreeActivate = true;
                  StartCoroutine(ColliderOff(other.gameObject));
              }
              else if (timerThreeActivate)
              {
                  totalTime.enabled = true;
                  timerThreeActivate = false;
                  calculateTotaleTime = true;
              }
          }
          if (other.gameObject.CompareTag("finish"))
          {
              calculateTotaleTime = true;
              timerThreeActivate = false;
              speedoMeter.SetActive(false);
              speedOmeter.SetActive(false);
              panel.Play("Panel");
              laps.Play("Laps");
              activateIt = true;
              finishCam.gameObject.SetActive(true);
              Time.timeScale = 0.05f;
              Time.fixedDeltaTime = Time.timeScale * 0.02f;
          }
      }
  
      public IEnumerator ColliderOn(GameObject trigger)
      {
          trigger.gameObject.SetActive(false);
          yield return new WaitForSeconds(2);
          trigger.gameObject.SetActive(true);
      }
      public IEnumerator ColliderOff(GameObject trigger)
      {
          trigger.gameObject.SetActive(false);
          yield return new WaitForSeconds(2);
          finish.gameObject.SetActive(true);
      }
  }

Again a lot of spaghetti copy paste code. I would have changed almost all variables to lists since the variables are also to much.



I added the speedometer to the game with the asset the artists provided me that indicates realtime speed in km/h, throttle and brake.



After all important mechanics were finished I used my spare time to create some animations for the game and main menu and also creating a short cutscene for when u finish the race and a starting screen showcasing some racing moments from formula 1.





Date: Jan 28, 2022

– Project: Night Racer

– Duration: 2 Week

– Team: 1 Dev, 2 Artists

My Part: Car Controller, Camera Changer, Wheel physics, Lap system, Menu Scroller, Speedometer, Cutscene.

Summary:

Good controllable vehicle, Full Gameplay loop, Time trials must be able to be saved. A round lasts a maximum of 2 minutes, Working User Interface, Countdown / traffic light, before you can race, Speedometer, lap time, fastest time achieved on your computer, overview screen achieved times.